package gagak.framework.web.paramresovler;

import gagak.framework.web.exception.HttpMessageNotWritableException;
import gagak.framework.web.paramresovler.converter.GenericHttpMessageConverter;
import gagak.framework.web.paramresovler.converter.json.ContentNegotiationManager;
import gagak.framework.web.paramresovler.requestbody.*;
import gagak.framework.web.annonation.Nullable;
import gagak.framework.web.handler.HandlerMapping;
import gagak.framework.web.param.MethodParameter;
import gagak.framework.web.reflect.ResolvableType;
import gagak.framework.web.exception.HttpMediaTypeNotAcceptableException;
import gagak.framework.web.support.http.HttpHeaders;
import gagak.framework.web.paramresovler.resonsebody.ServletServerHttpResponse;
import gagak.framework.web.support.http.MediaType;
import gagak.framework.web.support.request.NativeWebRequest;
import gagak.framework.web.support.request.ServletWebRequest;
import gagak.framework.web.utils.Assert;
import gagak.framework.web.utils.CollectionUtils;
import gagak.framework.web.utils.StringUtils;
import com.g5.framework.web.support.UrlPathHelper;
import org.apache.http.HttpEntity;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.*;

/*
 * @program: ce
 * @description:
 * @author: houhong
 * @create: 2022-11-30 20:50
 */
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver {


    // 默认情况下：文件们后缀是这些就不弹窗下载
    private static final Set<String> WHITELISTED_EXTENSIONS = new HashSet<String>(Arrays.asList(
            "txt", "text", "yml", "properties", "csv",
            "json", "xml", "atom", "rss",
            "png", "jpe", "jpeg", "jpg", "gif", "wbmp", "bmp"));


    // 内容协商管理器
    private final ContentNegotiationManager contentNegotiationManager;



    private static final Set<String> WHITELISTED_MEDIA_BASE_TYPES = new HashSet<String>(
            Arrays.asList("audio", "image", "video"));


    private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application");

   /* private static final Type RESOURCE_REGION_LIST_TYPE =
            new ParameterizedTypeReference<List<ResourceRegion>>() { }.getType();*/

    private final Set<String> safeExtensions = new HashSet<String>();

    protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
                                                      @Nullable ContentNegotiationManager contentNegotiationManager) {

        this(converters, contentNegotiationManager, null);
    }

    // 可以指定内容协商管理器ContentNegotiationManager
    protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) {
        this(converters, null, null);
    }

    protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
                                                      @Nullable ContentNegotiationManager manager,
                                                      @Nullable List<Object> requestResponseBodyAdvice) {
        /*
         *  // TODO: 2022/11/19
         *        父类是 AbstractMessageConverMethodArgumentResolver  用来做模版方法，其中读取 HttInputpMessage
         * 			   处理了 被 @ControllerAdvice 标注的程序员拓展的东西
         *
         */
        super(converters);

        // 可以看到：默认情况下会直接new一个
        this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
        // this.pathStrategy = initPathStrategy(this.contentNegotiationManager);


        // 用safeExtensions装上内容协商所支持的所有后缀
        // 并且把后缀白名单也加上去（表示是默认支持的后缀）
        this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
        this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
    }

    /*
     * 这个方法是消息处理的核心之核心：处理了contentType、消息转换、内容协商、下载等等
     */
    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType, NativeWebRequest webRequest)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
        writeWithMessageConverters(value, returnType, inputMessage, outputMessage);
    }


    protected ServletServerHttpResponse createOutputMessage(NativeWebRequest webRequest) {
        HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
        Assert.state(response != null, "No HttpServletResponse");
        return new ServletServerHttpResponse(response);
    }


    protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
                                                  ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

        Object outputValue;
        Class<?> valueType;
        Type declaredType;

        if (value instanceof CharSequence) {
            outputValue = value.toString();
            valueType = String.class;
            declaredType = String.class;
        } else {
            outputValue = value;
            valueType = getReturnValueType(outputValue, returnType);
            declaredType = getGenericType(returnType);
        }


        List<MediaType> mediaTypesToUse;

        MediaType contentType = outputMessage.getHeaders().getContentType();

        if (contentType != null && contentType.isConcrete()) {
            mediaTypesToUse = Collections.singletonList(contentType);
        } else {
            HttpServletRequest request = inputMessage.getServletRequest();
            List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
            List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);

            if (outputValue != null && producibleMediaTypes.isEmpty()) {
                throw new HttpMessageNotWritableException(
                        "No converter found for return value of type: " + valueType);
            }
            mediaTypesToUse = new ArrayList<MediaType>();
            for (MediaType requestedType : requestedMediaTypes) {
                for (MediaType producibleType : producibleMediaTypes) {
                    if (requestedType.isCompatibleWith(producibleType)) {
                        mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }
            if (mediaTypesToUse.isEmpty()) {
                if (outputValue != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
                }
                return;
            }
            //  MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
        }

        MediaType selectedMediaType = null;
        for (MediaType mediaType : mediaTypesToUse) {
            if (mediaType.isConcrete()) {
                selectedMediaType = mediaType;
                break;
            } else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
                selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                break;
            }
        }

        if (selectedMediaType != null) {
            selectedMediaType = selectedMediaType.removeQualityValue();
            for (HttpMessageConverter<?> converter : this.httpMessageConverters) {
                GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
                        (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ?
                        ((GenericHttpMessageConverter) converter).canWrite(declaredType, valueType, selectedMediaType) :
                        converter.canWrite(valueType, selectedMediaType)) {

                    /*   outputValue = getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,
                            (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
                            inputMessage, outputMessage);*/
                    if (outputValue != null) {
                        addContentDispositionHeader(inputMessage, outputMessage);
                        if (genericConverter != null) {
                            genericConverter.write(outputValue, declaredType, selectedMediaType, outputMessage);
                        } else {
                            ((HttpMessageConverter) converter).write(outputValue, selectedMediaType, outputMessage);
                        }

                        logger.info("Written [" + outputValue + "] as \"" + selectedMediaType +
                                "\" using [" + converter + "]");

                    }
                    return;
                }
            }
        }

        if (outputValue != null) {
            throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
        }
    }


    protected Class<?> getReturnValueType(Object value, MethodParameter returnType) {
        return (value != null ? value.getClass() : returnType.getParameterType());
    }

    private Type getGenericType(MethodParameter returnType) {
        if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
            return ResolvableType.forType(returnType.getGenericParameterType()).getGeneric().getType();
        } else {
            return returnType.getGenericParameterType();
        }
    }



    private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
            throws HttpMediaTypeNotAcceptableException {

        return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
    }


    protected List<MediaType> getProducibleMediaTypes(
            HttpServletRequest request, Class<?> valueClass, @Nullable Type declaredType) {

        Set<MediaType> mediaTypes =
                (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
        if (!CollectionUtils.isEmpty(mediaTypes)) {
            return new ArrayList<MediaType>(mediaTypes);
        }
        else if (!this.allSupportedMediaTypes.isEmpty()) {
            List<MediaType> result = new ArrayList<MediaType>();
            for (HttpMessageConverter<?> converter : this.httpMessageConverters) {
                if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
                    if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
                        result.addAll(converter.getSupportedMediaTypes());
                    }
                }
                else if (converter.canWrite(valueClass, null)) {
                    result.addAll(converter.getSupportedMediaTypes());
                }
            }
            return result;
        }
        else {
            return Collections.singletonList(MediaType.ALL);
        }
    }


    private MediaType getMostSpecificMediaType(MediaType acceptType, MediaType produceType) {
        MediaType produceTypeToUse = produceType.copyQualityValue(acceptType);
        return (MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceTypeToUse) <= 0 ? acceptType : produceTypeToUse);
    }

    private void addContentDispositionHeader(ServletServerHttpRequest request, ServletServerHttpResponse response) {

        HttpHeaders headers = response.getHeaders();
        if (headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) {
            return;
        }

        try {
            int status = response.getServletResponse().getStatus();
            if (status < 200 || status > 299) {
                return;
            }
        }
        catch (Throwable ex) {
            // ignore
        }

        HttpServletRequest servletRequest = request.getServletRequest();
        String requestUri = UrlPathHelper.rawPathInstance.getOriginatingRequestUri(servletRequest);

        int index = requestUri.lastIndexOf('/') + 1;
        String filename = requestUri.substring(index);
        String pathParams = "";

        index = filename.indexOf(';');
        if (index != -1) {
            pathParams = filename.substring(index);
            filename = filename.substring(0, index);
        }

        filename = UrlPathHelper.defaultInstance.decodeRequestString(servletRequest, filename);
        String ext = StringUtils.getFilenameExtension(filename);

        pathParams = UrlPathHelper.defaultInstance.decodeRequestString(servletRequest, pathParams);
        String extInPathParams = StringUtils.getFilenameExtension(pathParams);

      /*  if (!safeExtension(servletRequest, ext) || !safeExtension(servletRequest, extInPathParams)) {
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=f.txt");
        }*/
    }





}